Completed
Push — master ( 8967bc...e88c19 )
by Rain
02:47
created

Selector.js ➔ ???   B

Complexity

Conditions 1

Size

Total Lines 260

Duplication

Lines 0
Ratio 0 %

Importance

Changes 16
Bugs 0 Features 0
Metric Value
cc 1
c 16
b 0
f 0
dl 0
loc 260
rs 8.2857

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
2
import $ from '$';
3
import _ from '_';
4
import key from 'key';
5
import ko from 'ko';
6
import {EventKeyCode} from 'Common/Enums';
7
import {isArray, inArray, noop, noopTrue} from 'Common/Utils';
8
9
class Selector
10
{
11
	/**
12
	 * @constructor
13
	 * @param {koProperty} koList
14
	 * @param {koProperty} koSelectedItem
15
	 * @param {koProperty} koFocusedItem
16
	 * @param {string} sItemSelector
17
	 * @param {string} sItemSelectedSelector
18
	 * @param {string} sItemCheckedSelector
19
	 * @param {string} sItemFocusedSelector
20
	 */
21
	constructor(koList, koSelectedItem, koFocusedItem,
22
		sItemSelector, sItemSelectedSelector, sItemCheckedSelector, sItemFocusedSelector)
23
	{
24
		this.list = koList;
25
26
		this.listChecked = ko.computed(() => _.filter(this.list(), (item) => item.checked())).extend({rateLimit: 0});
27
		this.isListChecked = ko.computed(() => 0 < this.listChecked().length);
28
29
		this.focusedItem = koFocusedItem || ko.observable(null);
30
		this.selectedItem = koSelectedItem || ko.observable(null);
31
		this.selectedItemUseCallback = true;
32
33
		this.itemSelectedThrottle = _.debounce(_.bind(this.itemSelected, this), 300);
34
35
		this.listChecked.subscribe((items) => {
36
			if (0 < items.length)
37
			{
38
				if (null === this.selectedItem())
39
				{
40
					if (this.selectedItem.valueHasMutated)
41
					{
42
						this.selectedItem.valueHasMutated();
43
					}
44
				}
45
				else
46
				{
47
					this.selectedItem(null);
48
				}
49
			}
50
			else if (this.autoSelect() && this.focusedItem())
51
			{
52
				this.selectedItem(this.focusedItem());
53
			}
54
		}, this);
55
56
		this.selectedItem.subscribe((item) => {
57
58
			if (item)
59
			{
60
				if (this.isListChecked())
61
				{
62
					_.each(this.listChecked(), (subItem) => {
63
						subItem.checked(false);
64
					});
65
				}
66
67
				if (this.selectedItemUseCallback)
68
				{
69
					this.itemSelectedThrottle(item);
70
				}
71
			}
72
			else if (this.selectedItemUseCallback)
73
			{
74
				this.itemSelected(null);
75
			}
76
77
		}, this);
78
79
		this.selectedItem = this.selectedItem.extend({toggleSubscribe: [null,
80
			(prev) => {
81
				if (prev)
82
				{
83
					prev.selected(false);
84
				}
85
			}, (next) => {
86
				if (next)
87
				{
88
					next.selected(true);
89
				}
90
			}
91
		]});
92
93
		this.focusedItem = this.focusedItem.extend({toggleSubscribe: [null,
94
			(prev) => {
95
				if (prev)
96
				{
97
					prev.focused(false);
98
				}
99
			}, (next) => {
100
				if (next)
101
				{
102
					next.focused(true);
103
				}
104
			}
105
		]});
106
107
		this.iSelectNextHelper = 0;
108
		this.iFocusedNextHelper = 0;
109
		this.oContentVisible = null;
110
		this.oContentScrollable = null;
111
112
		this.sItemSelector = sItemSelector;
113
		this.sItemSelectedSelector = sItemSelectedSelector;
114
		this.sItemCheckedSelector = sItemCheckedSelector;
115
		this.sItemFocusedSelector = sItemFocusedSelector;
116
117
		this.sLastUid = '';
118
		this.oCallbacks = {};
119
120
		this.focusedItem.subscribe((item) => {
121
			if (item)
122
			{
123
				this.sLastUid = this.getItemUid(item);
124
			}
125
		}, this);
126
127
		let
128
			aCache = [],
129
			aCheckedCache = [],
130
			mFocused = null,
131
			mSelected = null;
132
133
		this.list.subscribe((items) => {
134
135
			if (isArray(items))
136
			{
137
				_.each(items, (item) => {
138
					if (item)
139
					{
140
						const uid = this.getItemUid(item);
141
142
						aCache.push(uid);
143
						if (item.checked())
144
						{
145
							aCheckedCache.push(uid);
146
						}
147
						if (null === mFocused && item.focused())
148
						{
149
							mFocused = uid;
150
						}
151
						if (null === mSelected && item.selected())
152
						{
153
							mSelected = uid;
154
						}
155
					}
156
				});
157
			}
158
		}, this, 'beforeChange');
159
160
		this.list.subscribe((aItems) => {
161
162
			var
163
				oTemp = null,
164
				bGetNext = false,
165
				aUids = [],
166
				mNextFocused = mFocused,
167
				bChecked = false,
168
				bSelected = false,
169
				iLen = 0;
170
171
			this.selectedItemUseCallback = false;
172
173
			this.focusedItem(null);
174
			this.selectedItem(null);
175
176
			if (isArray(aItems))
177
			{
178
				iLen = aCheckedCache.length;
179
180
				_.each(aItems, (oItem) => {
181
182
					var sUid = this.getItemUid(oItem);
183
					aUids.push(sUid);
184
185
					if (null !== mFocused && mFocused === sUid)
186
					{
187
						this.focusedItem(oItem);
188
						mFocused = null;
189
					}
190
191
					if (0 < iLen && -1 < inArray(sUid, aCheckedCache))
192
					{
193
						bChecked = true;
194
						oItem.checked(true);
195
						iLen -= 1;
196
					}
197
198
					if (!bChecked && null !== mSelected && mSelected === sUid)
199
					{
200
						bSelected = true;
201
						this.selectedItem(oItem);
202
						mSelected = null;
203
					}
204
				});
205
206
				this.selectedItemUseCallback = true;
207
208
				if (!bChecked && !bSelected && this.autoSelect())
209
				{
210
					if (this.focusedItem())
211
					{
212
						this.selectedItem(this.focusedItem());
213
					}
214
					else if (0 < aItems.length)
215
					{
216
						if (null !== mNextFocused)
217
						{
218
							bGetNext = false;
219
							mNextFocused = _.find(aCache, (sUid) => {
220
								if (bGetNext && -1 < inArray(sUid, aUids))
221
								{
222
									return sUid;
223
								}
224
								else if (mNextFocused === sUid)
225
								{
226
									bGetNext = true;
227
								}
228
								return false;
229
							});
230
231
							if (mNextFocused)
232
							{
233
								oTemp = _.find(aItems, (oItem) => mNextFocused === this.getItemUid(oItem));
234
							}
235
						}
236
237
						this.selectedItem(oTemp || null);
238
						this.focusedItem(this.selectedItem());
239
					}
240
				}
241
242
				if ((0 !== this.iSelectNextHelper || 0 !== this.iFocusedNextHelper) && 0 < aItems.length && !this.focusedItem())
243
				{
244
					oTemp = null;
245
					if (0 !== this.iFocusedNextHelper)
246
					{
247
						oTemp = aItems[-1 === this.iFocusedNextHelper ? aItems.length - 1 : 0] || null;
248
					}
249
250
					if (!oTemp && 0 !== this.iSelectNextHelper)
251
					{
252
						oTemp = aItems[-1 === this.iSelectNextHelper ? aItems.length - 1 : 0] || null;
253
					}
254
255
					if (oTemp)
256
					{
257
						if (0 !== this.iSelectNextHelper)
258
						{
259
							this.selectedItem(oTemp || null);
260
						}
261
262
						this.focusedItem(oTemp || null);
263
264
						this.scrollToFocused();
265
266
						_.delay(() => this.scrollToFocused(), 100);
267
					}
268
269
					this.iSelectNextHelper = 0;
270
					this.iFocusedNextHelper = 0;
271
				}
272
			}
273
274
			aCache = [];
275
			aCheckedCache = [];
276
			mFocused = null;
277
			mSelected = null;
278
279
		}, this);
280
	}
281
282
	itemSelected(item) {
283
284
		if (this.isListChecked())
285
		{
286
			if (!item)
287
			{
288
				(this.oCallbacks.onItemSelect || noop)(item || null);
289
			}
290
		}
291
		else
292
		{
293
			if (item)
294
			{
295
				(this.oCallbacks.onItemSelect || noop)(item);
296
			}
297
		}
298
	}
299
300
	/**
301
	 * @param {boolean} forceSelect
302
	 */
303
	goDown(forceSelect) {
304
		this.newSelectPosition(EventKeyCode.Down, false, forceSelect);
305
	}
306
307
	/**
308
	 * @param {boolean} forceSelect
309
	 */
310
	goUp(forceSelect) {
311
		this.newSelectPosition(EventKeyCode.Up, false, forceSelect);
312
	}
313
314
	unselect() {
315
		this.selectedItem(null);
316
		this.focusedItem(null);
317
	}
318
319
	init(contentVisible, contentScrollable, keyScope = 'all') {
320
321
		this.oContentVisible = contentVisible;
322
		this.oContentScrollable = contentScrollable;
323
324
		if (this.oContentVisible && this.oContentScrollable)
325
		{
326
			$(this.oContentVisible)
327
				.on('selectstart', (event) => {
328
					if (event && event.preventDefault)
329
					{
330
						event.preventDefault();
331
					}
332
				})
333
				.on('click', this.sItemSelector, (event) => {
334
					this.actionClick(ko.dataFor(event.currentTarget), event);
335
				})
336
				.on('click', this.sItemCheckedSelector, (event) => {
337
					const item = ko.dataFor(event.currentTarget);
338
					if (item)
339
					{
340
						if (event && event.shiftKey)
341
						{
342
							this.actionClick(item, event);
343
						}
344
						else
345
						{
346
							this.focusedItem(item);
347
							item.checked(!item.checked());
348
						}
349
					}
350
				});
351
352
			key('enter', keyScope, () => {
353
				if (this.focusedItem() && !this.focusedItem().selected())
354
				{
355
					this.actionClick(this.focusedItem());
356
					return false;
357
				}
358
359
				return true;
360
			});
361
362
			key('ctrl+up, command+up, ctrl+down, command+down', keyScope, () => false);
363
364
			key('up, shift+up, down, shift+down, home, end, pageup, pagedown, insert, space', keyScope, (event, handler) => {
365
				if (event && handler && handler.shortcut)
366
				{
367
					let eventKey = 0;
368
					switch (handler.shortcut)
369
					{
370
						case 'up':
371
						case 'shift+up':
372
							eventKey = EventKeyCode.Up;
373
							break;
374
						case 'down':
375
						case 'shift+down':
376
							eventKey = EventKeyCode.Down;
377
							break;
378
						case 'insert':
379
							eventKey = EventKeyCode.Insert;
380
							break;
381
						case 'space':
382
							eventKey = EventKeyCode.Space;
383
							break;
384
						case 'home':
385
							eventKey = EventKeyCode.Home;
386
							break;
387
						case 'end':
388
							eventKey = EventKeyCode.End;
389
							break;
390
						case 'pageup':
391
							eventKey = EventKeyCode.PageUp;
392
							break;
393
						case 'pagedown':
394
							eventKey = EventKeyCode.PageDown;
395
							break;
396
						// no default
397
					}
398
399
					if (0 < eventKey)
400
					{
401
						this.newSelectPosition(eventKey, key.shift);
402
						return false;
403
					}
404
				}
405
406
				return true;
407
			});
408
		}
409
	}
410
411
	/**
412
	 * @returns {boolean}
413
	 */
414
	autoSelect() {
415
		return !!(this.oCallbacks.onAutoSelect || noopTrue)();
416
	}
417
418
	/**
419
	 * @param {boolean} up
420
	 */
421
	doUpUpOrDownDown(up) {
422
		(this.oCallbacks.onUpUpOrDownDown || noopTrue)(!!up);
423
	}
424
425
	/**
426
	 * @param {Object} oItem
0 ignored issues
show
Documentation introduced by
The parameter oItem does not exist. Did you maybe forget to remove this comment?
Loading history...
427
	 * @returns {string}
428
	 */
429
	getItemUid(item) {
430
431
		let uid = '';
432
433
		const getItemUidCallback = this.oCallbacks.onItemGetUid || null;
434
		if (getItemUidCallback && item)
435
		{
436
			uid = getItemUidCallback(item);
437
		}
438
439
		return uid.toString();
440
	}
441
442
	/**
443
	 * @param {number} iEventKeyCode
444
	 * @param {boolean} bShiftKey
445
	 * @param {boolean=} bForceSelect = false
446
	 */
447
	newSelectPosition(iEventKeyCode, bShiftKey, bForceSelect) {
448
449
		var
450
			iIndex = 0,
451
			iPageStep = 10,
452
			bNext = false,
453
			bStop = false,
454
			oResult = null,
455
			aList = this.list(),
456
			iListLen = aList ? aList.length : 0,
457
			oFocused = this.focusedItem();
458
459
		if (0 < iListLen)
460
		{
461
			if (!oFocused)
462
			{
463
				if (EventKeyCode.Down === iEventKeyCode || EventKeyCode.Insert === iEventKeyCode ||
464
					EventKeyCode.Space === iEventKeyCode || EventKeyCode.Home === iEventKeyCode ||
465
					EventKeyCode.PageUp === iEventKeyCode)
466
				{
467
					oResult = aList[0];
468
				}
469
				else if (EventKeyCode.Up === iEventKeyCode || EventKeyCode.End === iEventKeyCode ||
470
					EventKeyCode.PageDown === iEventKeyCode)
471
				{
472
					oResult = aList[aList.length - 1];
473
				}
474
			}
475
			else if (oFocused)
476
			{
477
				if (EventKeyCode.Down === iEventKeyCode || EventKeyCode.Up === iEventKeyCode ||
478
					EventKeyCode.Insert === iEventKeyCode || EventKeyCode.Space === iEventKeyCode)
479
				{
480
					_.each(aList, (item) => {
481
						if (!bStop)
482
						{
483
							switch (iEventKeyCode)
484
							{
485
								case EventKeyCode.Up:
486
									if (oFocused === item)
487
									{
488
										bStop = true;
489
									}
490
									else
491
									{
492
										oResult = item;
493
									}
494
									break;
495
								case EventKeyCode.Down:
496
								case EventKeyCode.Insert:
497
									if (bNext)
498
									{
499
										oResult = item;
500
										bStop = true;
501
									}
502
									else if (oFocused === item)
503
									{
504
										bNext = true;
505
									}
506
									break;
507
								// no default
508
							}
509
						}
510
					});
511
512
					if (!oResult && (EventKeyCode.Down === iEventKeyCode || EventKeyCode.Up === iEventKeyCode))
513
					{
514
						this.doUpUpOrDownDown(EventKeyCode.Up === iEventKeyCode);
515
					}
516
				}
517
				else if (EventKeyCode.Home === iEventKeyCode || EventKeyCode.End === iEventKeyCode)
518
				{
519
					if (EventKeyCode.Home === iEventKeyCode)
520
					{
521
						oResult = aList[0];
522
					}
523
					else if (EventKeyCode.End === iEventKeyCode)
524
					{
525
						oResult = aList[aList.length - 1];
526
					}
527
				}
528
				else if (EventKeyCode.PageDown === iEventKeyCode)
529
				{
530
					for (; iIndex < iListLen; iIndex++)
531
					{
532
						if (oFocused === aList[iIndex])
533
						{
534
							iIndex += iPageStep;
535
							iIndex = iListLen - 1 < iIndex ? iListLen - 1 : iIndex;
536
							oResult = aList[iIndex];
537
							break;
538
						}
539
					}
540
				}
541
				else if (EventKeyCode.PageUp === iEventKeyCode)
542
				{
543
					for (iIndex = iListLen; 0 <= iIndex; iIndex--)
544
					{
545
						if (oFocused === aList[iIndex])
546
						{
547
							iIndex -= iPageStep;
548
							iIndex = 0 > iIndex ? 0 : iIndex;
549
							oResult = aList[iIndex];
550
							break;
551
						}
552
					}
553
				}
554
			}
555
		}
556
557
		if (oResult)
558
		{
559
			this.focusedItem(oResult);
560
561
			if (oFocused)
562
			{
563
				if (bShiftKey)
564
				{
565
					if (EventKeyCode.Up === iEventKeyCode || EventKeyCode.Down === iEventKeyCode)
566
					{
567
						oFocused.checked(!oFocused.checked());
568
					}
569
				}
570
				else if (EventKeyCode.Insert === iEventKeyCode || EventKeyCode.Space === iEventKeyCode)
571
				{
572
					oFocused.checked(!oFocused.checked());
573
				}
574
			}
575
576
			if ((this.autoSelect() || !!bForceSelect) &&
577
				!this.isListChecked() && EventKeyCode.Space !== iEventKeyCode)
578
			{
579
				this.selectedItem(oResult);
580
			}
581
582
			this.scrollToFocused();
583
		}
584
		else if (oFocused)
585
		{
586
			if (bShiftKey && (EventKeyCode.Up === iEventKeyCode || EventKeyCode.Down === iEventKeyCode))
587
			{
588
				oFocused.checked(!oFocused.checked());
589
			}
590
			else if (EventKeyCode.Insert === iEventKeyCode || EventKeyCode.Space === iEventKeyCode)
591
			{
592
				oFocused.checked(!oFocused.checked());
593
			}
594
595
			this.focusedItem(oFocused);
596
		}
597
	}
598
599
	/**
600
	 * @returns {boolean}
601
	 */
602
	scrollToFocused() {
603
604
		if (!this.oContentVisible || !this.oContentScrollable)
605
		{
606
			return false;
607
		}
608
609
		const
610
			offset = 20,
611
			list = this.list(),
612
			$focused = $(this.sItemFocusedSelector, this.oContentScrollable),
613
			pos = $focused.position(),
614
			visibleHeight = this.oContentVisible.height(),
615
			focusedHeight = $focused.outerHeight();
616
617
		if (list && list[0] && list[0].focused())
618
		{
619
			this.oContentScrollable.scrollTop(0);
620
			return true;
621
		}
622
		else if (pos && (0 > pos.top || pos.top + focusedHeight > visibleHeight))
623
		{
624
			this.oContentScrollable.scrollTop(0 > pos.top ?
625
				this.oContentScrollable.scrollTop() + pos.top - offset :
626
				this.oContentScrollable.scrollTop() + pos.top - visibleHeight + focusedHeight + offset
627
			);
628
629
			return true;
630
		}
631
632
		return false;
633
	}
634
635
	/**
636
	 * @param {boolean=} fast = false
637
	 * @returns {boolean}
638
	 */
639
	scrollToTop(fast = false) {
640
641
		if (!this.oContentVisible || !this.oContentScrollable)
642
		{
643
			return false;
644
		}
645
646
		if (fast || 50 > this.oContentScrollable.scrollTop())
647
		{
648
			this.oContentScrollable.scrollTop(0);
649
		}
650
		else
651
		{
652
			this.oContentScrollable.stop().animate({scrollTop: 0}, 200);
653
		}
654
655
		return true;
656
	}
657
658
	eventClickFunction(item, event) {
659
660
		let
661
			index = 0,
662
			length = 0,
663
			changeRange = false,
0 ignored issues
show
Unused Code introduced by
The assignment to variable changeRange seems to be never used. Consider removing it.
Loading history...
664
			isInRange = false,
665
			list = [],
666
			checked = false,
667
			listItem = null,
0 ignored issues
show
Unused Code introduced by
The assignment to listItem seems to be never used. If you intend to free memory here, this is not necessary since the variable leaves the scope anyway.
Loading history...
668
			lineUid = '';
669
670
		const uid = this.getItemUid(item);
671
		if (event && event.shiftKey)
672
		{
673
			if ('' !== uid && '' !== this.sLastUid && uid !== this.sLastUid)
674
			{
675
				list = this.list();
676
				checked = item.checked();
677
678
				for (index = 0, length = list.length; index < length; index++)
679
				{
680
					listItem = list[index];
681
					lineUid = this.getItemUid(listItem);
682
683
					changeRange = false;
684
					if (lineUid === this.sLastUid || lineUid === uid)
685
					{
686
						changeRange = true;
687
					}
688
689
					if (changeRange)
690
					{
691
						isInRange = !isInRange;
692
					}
693
694
					if (isInRange || changeRange)
695
					{
696
						listItem.checked(checked);
697
					}
698
				}
699
			}
700
		}
701
702
		this.sLastUid = '' === uid ? '' : uid;
703
	}
704
705
	/**
706
	 * @param {Object} item
707
	 * @param {Object=} event
708
	 */
709
	actionClick(item, event = null) {
710
711
		if (item)
712
		{
713
			let click = true;
714
			if (event)
715
			{
716
				if (event.shiftKey && !(event.ctrlKey || event.metaKey) && !event.altKey)
717
				{
718
					click = false;
719
					if ('' === this.sLastUid)
720
					{
721
						this.sLastUid = this.getItemUid(item);
722
					}
723
724
					item.checked(!item.checked());
725
					this.eventClickFunction(item, event);
726
727
					this.focusedItem(item);
728
				}
729
				else if ((event.ctrlKey || event.metaKey) && !event.shiftKey && !event.altKey)
730
				{
731
					click = false;
732
					this.focusedItem(item);
733
734
					if (this.selectedItem() && item !== this.selectedItem())
735
					{
736
						this.selectedItem().checked(true);
737
					}
738
739
					item.checked(!item.checked());
740
				}
741
			}
742
743
			if (click)
744
			{
745
				this.selectMessageItem(item);
746
			}
747
		}
748
	}
749
750
	on(eventName, callback) {
751
		this.oCallbacks[eventName] = callback;
752
	}
753
754
	selectMessageItem(messageItem) {
755
		this.focusedItem(messageItem);
756
		this.selectedItem(messageItem);
757
		this.scrollToFocused();
758
	}
759
}
760
761
export {Selector, Selector as default};
762